﻿#region License
// (c) Intergen.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using TextGlow.Control.Model;
using TextGlow.Control.Utilities;
using TextGlow.Control.Packaging;
using System.IO;
using System.Xml;
using System.Collections.ObjectModel;

namespace TextGlow.Control.Model
{
  public class Document : Content
  {
    public static readonly XNamespace WordprocessingML = XNamespace.Get("http://schemas.openxmlformats.org/wordprocessingml/2006/main");
    public static readonly XNamespace WordprocessingDrawingML = XNamespace.Get("http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing");
    public static readonly XNamespace DrawingML = XNamespace.Get("http://schemas.openxmlformats.org/drawingml/2006/main");
    public static readonly XNamespace PictureDrawingML = XNamespace.Get("http://schemas.openxmlformats.org/drawingml/2006/picture");
    public static readonly XNamespace Relationship = XNamespace.Get("http://schemas.openxmlformats.org/officeDocument/2006/relationships");

    private readonly Package _package;
    private readonly IPackagePart _part;
    private List<Section> _sections = new List<Section>();
    private readonly KeyedCollection<string, Style> _styles = new DelegateKeyedCollection<string, Style>(s => s.Id);
    private readonly KeyedCollection<string, Font> _fonts = new DelegateKeyedCollection<string, Font>(f => f.Name);
    private readonly KeyedCollection<int, NumberingDefinition> _numberingDefinitions = new DelegateKeyedCollection<int, NumberingDefinition>(i => i.NumberId);
    private readonly KeyedCollection<string, Header> _headers = new DelegateKeyedCollection<string, Header>(f => f.Id);
    private readonly KeyedCollection<string, Footer> _footers = new DelegateKeyedCollection<string, Footer>(f => f.Id);
    private ParagraphProperties _defaultParagraphProperties;
    private RunProperties _defaultRunProperties;

    private readonly KeyedCollection<string, OpenDocumentStyle> _openDocumentStyles = new DelegateKeyedCollection<string, OpenDocumentStyle>(s => s.Name);

    public KeyedCollection<string, OpenDocumentStyle> OpenDocumentStyles
    {
      get { return _openDocumentStyles; }
    } 

    private Unit _defaultTabStop;
    private bool _evenAndOddHeaders;
    private string _themeMajorFont;
    private string _themeMinorFont;

    /// <summary>
    /// Gets or sets the theme major font.
    /// </summary>
    /// <value>The theme major font.</value>
    public string ThemeMajorFont
    {
      get { return _themeMajorFont; }
      set { _themeMajorFont = value; }
    }

    /// <summary>
    /// Gets or sets the theme minor font.
    /// </summary>
    /// <value>The theme minor font.</value>
    public string ThemeMinorFont
    {
      get { return _themeMinorFont; }
      set { _themeMinorFont = value; }
    }

    /// <summary>
    /// Gets or sets the default tab stop.
    /// </summary>
    /// <value>The default tab stop.</value>
    public Unit DefaultTabStop
    {
      get { return _defaultTabStop; }
      set { _defaultTabStop = value; }
    }

    public bool EvenAndOddHeaders
    {
      get { return _evenAndOddHeaders; }
      set { _evenAndOddHeaders = value; }
    }

    /// <summary>
    /// Gets the document's package.
    /// </summary>
    /// <value>The package.</value>
    public Package Package
    {
      get { return _package; }
    }

    public KeyedCollection<int, NumberingDefinition> NumberingDefinitions
    {
      get { return _numberingDefinitions; }
    }

    public KeyedCollection<string, Font> Fonts
    {
      get { return _fonts; }
    }

    public KeyedCollection<string, Style> Styles
    {
      get { return _styles; }
    }

    public KeyedCollection<string, Header> Headers
    {
      get { return _headers; }
    }

    public KeyedCollection<string, Footer> Footers
    {
      get { return _footers; }
    }

    public RunProperties DefaultRunProperties
    {
      get { return _defaultRunProperties; }
    }

    public ParagraphProperties DefaultParagraphProperties
    {
      get { return _defaultParagraphProperties; }
    }

    public override Document MainDocument
    {
      get { return this; }
    }

    public override IPackagePart Part
    {
      get { return _part; }
    }

    public List<Section> Sections
    {
      get { return _sections; }
    }

    public Document()
      : this(null)
    {
    }

    private Document(Package package)
      : base(null)
    {
      if (package != null)
      {
        _part = package.GetPart(PartType.MainDocument);

        if (_part == null)
          throw new ArgumentException("Package does not have main document part.", "package");

        _package = package;
      }
    }

    public void LoadHeaders()
    {
      IPackagePart mainDocumentPart = _package.GetPart(PartType.MainDocument);

      var headers = Part.Relationships
        .Where(r => r.RelationshipType == Header.RelationshipType)
        .Select(r => new Header(r.Id, this, mainDocumentPart.Parts[r.Id], XDocument.Parse(mainDocumentPart.Parts[r.Id].GetContent()).Root));

      _headers.AddRange(headers);
    }

    private void LoadFooters()
    {
      IPackagePart mainDocumentPart = _package.GetPart(PartType.MainDocument);

      var footers = Part.Relationships
        .Where(r => r.RelationshipType == Footer.RelationshipType)
        .Select(r => new Footer(r.Id, this, mainDocumentPart.Parts[r.Id], XDocument.Parse(mainDocumentPart.Parts[r.Id].GetContent()).Root));

      _footers.AddRange(footers);
    }

    public void LoadStyles(TextReader stylesDocumentReader)
    {
      if (stylesDocumentReader == null)
        throw new ArgumentNullException("stylesDocumentReader");

      XDocument stylesDocument = XDocument.Load(stylesDocumentReader);

      XNamespace ns = WordprocessingML;

      foreach (XElement styleElement in stylesDocument.Root.Elements(ns + "style"))
      {
        _styles.Add(new Style(this, styleElement));
      }

      XElement paragraphRunElement = stylesDocument.Root.Elements(ns + "docDefaults").Elements(ns + "rPrDefault").Elements(ns + "rPr").SingleOrDefault();
      if (paragraphRunElement != null)
        _defaultRunProperties = new RunProperties(this, paragraphRunElement);

      XElement paragraphPropertiesElement = stylesDocument.Root.Elements(ns + "docDefaults").Elements(ns + "pPrDefault").Elements(ns + "pPr").SingleOrDefault();
      if (paragraphPropertiesElement != null)
        _defaultParagraphProperties = new ParagraphProperties(this, paragraphPropertiesElement);
    }

    public void LoadSettings(TextReader settingsDocumentReader)
    {
      if (settingsDocumentReader == null)
        throw new ArgumentNullException("settingsDocumentReader");

      XDocument settingsDocument = XDocument.Load(settingsDocumentReader);

      XNamespace ns = WordprocessingML;

      _defaultTabStop = Unit.Twip(XmlUtils.GetValue<double>(settingsDocument.Root.Element(ns + "defaultTabStop"))).Value;
      _evenAndOddHeaders = XmlUtils.GetBoolean(settingsDocument.Root.Element(ns + "evenAndOddHeaders")).GetValueOrDefault();
    }

    public void LoadFonts(TextReader fontsDocumentReader)
    {
      if (fontsDocumentReader == null)
        throw new ArgumentNullException("fontsDocumentReader");

      XDocument fontsDocument = XDocument.Load(fontsDocumentReader);

      XNamespace ns = Document.WordprocessingML;

      foreach (XElement fontElement in fontsDocument.Root.Elements(ns + "font"))
      {
        _fonts.Add(new Font(fontElement));
      }
    }

    public void LoadTheme(StringReader themeDocumentReader)
    {
      if (themeDocumentReader == null)
        throw new ArgumentNullException("themeDocumentReader");

      XDocument themeDocument = XDocument.Load(themeDocumentReader);

      XNamespace ns = Document.DrawingML;

      XElement fontSchemeElement = themeDocument.Root.Element(ns + "themeElements").Element(ns + "fontScheme");

      _themeMajorFont = XmlUtils.GetValue(fontSchemeElement.Elements(ns + "majorFont").Elements(ns + "latin").SingleOrDefault(), XName.Get("typeface"));
      _themeMinorFont = XmlUtils.GetValue(fontSchemeElement.Elements(ns + "minorFont").Elements(ns + "latin").SingleOrDefault(), XName.Get("typeface"));
    }

    public void LoadMainDocument(TextReader mainDocumentReader)
    {
      if (mainDocumentReader == null)
        throw new ArgumentNullException("mainDocumentReader");

      XNamespace ns = Document.WordprocessingML;

      List<DocumentContent> content = null;
      SectionProperties finalSectionProperties = null;

      using (XmlReader reader = XmlReader.Create(mainDocumentReader))
      {
        while (reader.Read())
        {
          // move to body content
          if (reader.LocalName == "automatic-styles" && reader.NodeType == XmlNodeType.Element)
          {
            XElement automaticStylesElement = (XElement)XElement.ReadFrom(reader);

            IList<OpenDocumentStyle> styles = automaticStylesElement.Elements().Where(e => e.Name.LocalName == "style").Select(e => new OpenDocumentStyle(e)).ToList();
            _openDocumentStyles.AddRange(styles);
          }
          else if (reader.LocalName == "body" && reader.NodeType == XmlNodeType.Element)
          {
            content = DocumentContent.Create(this, GetChildContentElements(reader));

            if (reader.LocalName != "sectPr")
              throw new Exception("Final section properties element expected.");

            finalSectionProperties = new SectionProperties(this, (XElement)XElement.ReadFrom(reader));
          }
        }
      }

      // divide content up into sections
      _sections.AddRange(CreateSections(content, finalSectionProperties));
    }

    private IEnumerable<XElement> GetChildContentElements(XmlReader reader)
    {
      // move to first child
      reader.Read();

      while (true)
      {
        // skip whitespace between elements
        reader.MoveToContent();

        // break on end document section
        if (reader.LocalName == "sectPr")
          yield break;

        yield return (XElement)XElement.ReadFrom(reader);
      }
    }

    public void LoadNumbering(TextReader numberingDocumentReader)
    {
      if (numberingDocumentReader == null)
        throw new ArgumentNullException("numberingDocumentReader");

      XDocument numberingDocument = XDocument.Load(numberingDocumentReader);

      XNamespace ns = Document.WordprocessingML;
      XElement root = numberingDocument.Root;

      List<NumberingDefinition> definitions =
        (from ne in root.Elements(ns + "num")
         let abstractNumId = XmlUtils.GetValue(ne.Element(ns + "abstractNumId"))
         let abstractNumElement =
           (from ae in root.Elements(ns + "abstractNum")
            where (string)ae.Attribute(ns + "abstractNumId") == abstractNumId
            select ae)
            .Single()
         select new NumberingDefinition(this, ne, abstractNumElement))
         .ToList();

      _numberingDefinitions.AddRange(definitions);
    }

    private List<Section> CreateSections(List<DocumentContent> content, SectionProperties finalSectionProperties)
    {
      var sectionParagraphs =
        content.Select((c, i) => new { Paragraph = c as Paragraph, Index = i })
        // only want paragraphs with section properties
        .Where(o => o.Paragraph != null && o.Paragraph.Properties.SectionProperties != null)
        // get the section properties
        .Select(o => new { SectionProperties = o.Paragraph.Properties.SectionProperties, o.Index })
        // add the final section properties plus end index to result
        .Union(new[] { new { SectionProperties = finalSectionProperties, Index = content.Count - 1 } })
        .ToList();

      List<Section> sections = new List<Section>();
      int previousSectionEndIndex = -1;
      foreach (var sectionParagraph in sectionParagraphs)
      {
        List<DocumentContent> sectionContent =
          content.Select((c, i) => new { Content = c, Index = i })
          .Where(o => o.Index <= sectionParagraph.Index && o.Index > previousSectionEndIndex)
          .Select(o => o.Content)
          .ToList();

        Section section = new Section(this, sectionContent, sectionParagraph.SectionProperties);
        sections.Add(section);

        previousSectionEndIndex = sectionParagraph.Index;
      }

      return sections;
    }

    /// <summary>
    /// Creates a document using the given document package.
    /// </summary>
    /// <param name="documentPackage">The document package.</param>
    /// <returns>The document stored within the package.</returns>
    public static Document CreateDocument(Package documentPackage)
    {
      if (documentPackage == null)
        throw new ArgumentNullException("documentPackage");

      Document doc = new Document(documentPackage);

      IPackagePart mainDocumentPart = documentPackage.GetPart(PartType.MainDocument);
      doc.LoadMainDocument(new StringReader(mainDocumentPart.GetContent()));

      IPackagePart numberingDefinitions = mainDocumentPart.GetPart(PartType.NumberingDefinitions);
      if (numberingDefinitions != null)
        doc.LoadNumbering(new StringReader(numberingDefinitions.GetContent()));

      IPackagePart theme = mainDocumentPart.GetPart(PartType.Theme);
      if (theme != null)
        doc.LoadTheme(new StringReader(theme.GetContent()));

      IPackagePart styles = mainDocumentPart.GetPart(PartType.StyleDefinitions);
      doc.LoadStyles(new StringReader(styles.GetContent()));

      IPackagePart fonts = mainDocumentPart.GetPart(PartType.FontTable);
      doc.LoadFonts(new StringReader(fonts.GetContent()));

      IPackagePart settings = mainDocumentPart.GetPart(PartType.DocumentSettings);
      doc.LoadSettings(new StringReader(settings.GetContent()));

      doc.LoadHeaders();
      doc.LoadFooters();

      return doc;
    }
  }
}